Introduction
This document will provide a walk-through tutorial to use bleak to implement the Open GoPro Interface to send commands and receive responses.
Commands in this sense are specifically procedures that are initiated by either:
- writing to the Command Request UUID and received responses via the Command Response UUID. They are listed here.
- writing to the Setting UUID and received responses via the Setting Response UUID. They are listed here.
Note! It is required that you have first completed the connect tutorial before going through this tutorial.
This tutorial only considers sending these commands as one-off commands. That is, it does not consider state management / synchronization when sending multiple commands. This will be discussed in a future lab.
Requirements
It is assumed that that hardware and software requirements from the connect tutorial are present and configured correctly.
Just Show me the Demo(s)!!
Each of the commands detailed in Sending Commands has a corresponding script to demo it. If you don’t want to read this tutorial and just want to see the demo, For example, run:
$ python ble_command_set_shutter.py
Note! Python 3.8.x must be used as specified in the requirements
Note that each script has a command-line help which can be found via:
$ python ./ble_command_set_shutter.py --help
usage: ble_command_set_shutter.py [-h] [-i IDENTIFIER]
Connect to a GoPro camera, set the shutter, wait 2 seconds, then set the shutter off.
optional arguments:
-h, --help show this help message and exit
-i IDENTIFIER, --identifier IDENTIFIER
Last 4 digits of GoPro name to scan for. If not used, first discovered GoPro will be connected to
Setup
We must first connect as was discussed in the connect tutorial. In this case, however, we are defining a meaningful (albeit naive) notification handler that will:
- print byte data and handle that the notification was received on
- check if the response is what we expected
- set an event to notify the writer that the response was received
This is a very simple handler: response parsing will be expanded upon in the next tutorial.
def notification_handler(handle: int, data: bytes) -> None:
logger.info(f'Received response at {handle=}: {hexlify(data, ":")}')
# If this is the correct handle and the status is success, the command was a success
if client.services.characteristics[handle].uuid == response_uuid and data[2] == 0x00:
logger.info("Command sent successfully")
# Anything else is unexpected. This shouldn't happen
else:
logger.error("Unexpected response")
# Notify the writer
event.set()The event used above is a simple synchronization event that is only alerting the writer that a notification was received. There is much more to the synchronization and data parsing than this but this will be discussed in future tutorials. For now, we’re just checking that the handle matches what is expected and that the status (second byte) is success (0x00).
Command Overview
Both Command Requests and Setting Requests follow the same procedure:
- Write to relevant request UUID
- Receive confirmation from GoPro (via notification from relevant response UUID) that request was received.
- GoPro reacts to command
Note! The notification response only indicates that the request was received. The relevant behavior of the GoPro must be observed to verify when the command’s effects have been applied.
Here is the procedure from power-on to finish:
Sending Commands
Now that we are are connected, paired, and have enabled notifications (using our defined callback), we can send commands.
First, we need to define the attributes to write to and receive responses from which for commands are the “Command Request” characteristic (UUID b5f90072-aa8d-11e3-9046-0002a5d5c51b) and “Command Response” characteristic (UUID b5f90073-aa8d-11e3-9046-0002a5d5c51b).
COMMAND_REQ_UUID = GOPRO_BASE_UUID.format("0072")
COMMAND_RSP_UUID = GOPRO_BASE_UUID.format("0073")Or, for settings, the “Settings” characteristic (UUID b5f90074-aa8d-11e3-9046-0002a5d5c51b) and “Settings Response” (UUID b5f90075-aa8d-11e3-9046-0002a5d5c51b).
SETTINGS_REQ_UUID = GOPRO_BASE_UUID.format("0074")
SETTINGS_RSP_UUID = GOPRO_BASE_UUID.format("0075")Note! We’re using the GOPRO_BASE_UUID string imported from the module’s
__init__.pyto build these.
Set Shutter
The first command we will be sending is Set Shutter, which at byte level is:
| Command | Bytes |
|---|---|
| Set Shutter Off | 0x03 0x01 0x01 0x00 |
| Set Shutter On | 0x03 0x01 0x01 0x01 |
Set Shutter On
Now, let’s write the bytes to the “Command Request” UUID to turn the shutter on and start encoding!
event.clear()
await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 1]))
await event.wait() # Wait to receive the notification responseWe make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback.
You should hear the camera beep and it will either take a picture or start recording depending on what mode it is in.
Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications.. This can be seen in the demo log:
INFO:root:Setting the shutter on
INFO:root:Received response at handle=52: b'02:01:00'
INFO:root:Shutter command sent successfully
As expected, the response was received on the correct handle and the status was “success”.
If you are recording a video, go to the next tab to set the shutter off.
Set Shutter Off
We can now set the shutter off:
We’re waiting 2 seconds in case you are in video mode so that we can capture a 2 second video.
time.sleep(2)
event.clear()
await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([3, 1, 1, 0]))
await event.wait() # Wait to receive the notification responseThis will log in the console as follows:
INFO:root:Setting the shutter off
INFO:root:Received response at handle=52: b'02:01:00'
INFO:root:Shutter command sent successfullySleep
The next command we will be sending is Sleep, which at byte level is:
| Command | Bytes |
|---|---|
| Sleep | 0x01 0x05 |
Now, let’s write the bytes to the “Command Request UUID” to put the camera to sleep!
event.clear()
await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x01, 0x05]))
await event.wait() # Wait to receive the notification responseWe make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback.
You should hear the camera beep display a spinner showing “Powering Off”
Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications.. This can be seen in the demo log:
INFO:root:Putting the camera to sleep
INFO:root:Received response at handle=52: b'02:05:00'
INFO:root: Command sent successfully
As expected, the response was received on the correct handle and the status was “success”.
Since the camera has gone to sleep, it must be reconnected to via BLE to communicate again as detailed in the connect tutorial.
Load Preset Group
The next command we will be sending is Load Preset Group, which is used to toggle between the 3 groups of presets (video, photo, and timelapse). At byte level, the commands are:
| Command | Bytes |
|---|---|
| Load Video Preset Group | 0x04 0x3E 0x02 0x03 0xE8 |
| Load Photo Preset Group | 0x04 0x3E 0x02 0x03 0xE9 |
| Load Timelapse Preset Group | 0x04 0x3E 0x02 0x03 0xEA |
Now, let’s write the bytes to the “Command Request UUID” to change the preset group to Video!
event.clear()
await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x04, 0x3E, 0x02, 0x03, 0xE8]))
await event.wait() # Wait to receive the notification responseWe make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback.
You should hear the camera beep and move to the Video Preset Group. You can tell this by the logo at the top middle of the screen:
Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications.. This can be seen in the demo log:
INFO:root:Loading the video preset group...
INFO:root:Received response at handle=52: b'02:3e:00'
INFO:root:Command sent successfully
As expected, the response was received on the correct handle and the status was “success”.
Load Preset
The next command we will be sending is Load Preset, which is used to select a specific preset that is part of a Prest Group. At byte level, some of the commands for the various preset are:
| Command | Bytes |
|---|---|
| Load Cinematic Preset | 0x06 0x40 0x04 0x00 0x00 0x00 0x02 |
| Load Slo-Mo Preset | 0x06 0x40 0x04 0x00 0x00 0x00 0x03 |
| Load Burst Photo Preset | 0x06 0x40 0x04 0x00 0x01 0x00 0x02 |
| Load Night Photo Preset | 0x06 0x40 0x04 0x00 0x01 0x00 0x03 |
Now, let’s write the bytes to the “Command Request UUID” to change the preset to Cinematic!
event.clear()
await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x06, 0x40, 0x04, 0x00, 0x00, 0x00, 0x02]))
await event.wait() # Wait to receive the notification responseWe make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback.
You should hear the camera beep and switch to the Cinematic Preset (assuming it wasn’t already sent). You can verify this by seeing the preset name in the pill at bottom middle of the screen.
Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications.. This can be seen in the demo log:
INFO:root:Loading the cinematic preset...
INFO:root:Received response at handle=52: b'02:40:00'
INFO:root:Command sent successfully
As expected, the response was received on the correct handle and the status was “success”.
Enable Analytics
The next command we will be sending is Enable Analytics, which at byte level is:
| Command | Bytes |
|---|---|
| Enable Analytics | 0x01 0x50 |
This command is used to notify that a Third Party is using the Open GoPro API. It should be called after each connection.
Now, let’s write the bytes to the “Command Request UUID” to enable analytics.
event.clear()
await client.write_gatt_char(COMMAND_REQ_UUID, bytearray([0x01, 0x50]))
await event.wait() # Wait to receive the notification responseWe make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback.
Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications.. This can be seen in the demo log:
INFO:root:Enabling analytics...
INFO:root:Received response at handle=52: b'02:50:00'
INFO:root:Command sent successfully
As expected, the response was received on the correct handle and the status was “success”.
Set the Video Resolution
The next command we will be sending is Set Video Resolution. This is used to change the value of the Video Resolution setting. It is important to note that this only affects video resolution (not photo). Therefore, the Video Preset Group must be active in order for it to succeed. This can be done either manually through the camera UI or by sending Load Preset Group.
Here are some of the byte level commands for various video resolutions.
| Command | Bytes |
|---|---|
| Set Video Resolution to 1080 | 0x03 0x02 0x01 0x09 |
| Set Video Resolution to 2.7K | 0x03 0x02 0x01 0x04 |
| Set Video Resolution to 4K | 0x03 0x02 0x01 0x18 |
Now, let’s write the bytes to the “Setting Request UUID” to change the video resolution to 1080!
event.clear()
await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x02, 0x01, 0x09]))
await event.wait() # Wait to receive the notification responseWe make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback.
You should hear the camera beep and see the video resolution change to 1080 in the pill in the bottom-middle of the screen:
Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications.. This can be seen in the demo log:
INFO:root:Loading the video preset group...
INFO:root:Received response at handle=52: b'02:3e:00'
INFO:root:Command sent successfully
As expected, the response was received on the correct handle and the status was “success”. If the Preset Group was not Video, the status will not be success.
Set the Frames Per Second (FPS)
The next command we will be sending is Set FPS. This is used to change the value of the FPS setting. It is important to note that this setting is dependent on the video resolution. That is, certain FPS values are not valid with certain resolutions. In general, higher resolutions only allow lower FPS values. Check the camera capabilities to see which FPS values are valid for given use cases.
Therefore, for this step of the tutorial, it is assumed that the resolution has been set to 1080 as in Set the Video Resolution.
Here are some of the byte level commands for various FPS values.
| Command | Bytes |
|---|---|
| Set FPS to 24 | 0x03 0x03 0x01 0x0A |
| Set FPS to 60 | 0x03 0x03 0x01 0x05 |
| Set FPS to 240 | 0x03 0x03 0x01 0x00 |
Now, let’s write the bytes to the “Setting Request UUID” to change the FPS to 240!
event.clear()
await client.write_gatt_char(SETTINGS_REQ_UUID, bytearray([0x03, 0x03, 0x01, 0x00]))
await event.wait() # Wait to receive the notification responseWe make sure to clear the synchronization event before writing, then pend on the event until it is set in the notification callback.
You should hear the camera beep and see the FPS change to 240 in the pill in the bottom-middle of the screen:
Also note that we have received the “Command Status” notification response from the Command Response characteristic since we enabled it’s notifications in Enable Notifications.. This can be seen in the demo log:
INFO:root:Setting the fps to 240
INFO:root:Received response at handle=57: b'02:03:00'
INFO:root:Command sent successfully
As expected, the response was received on the correct handle and the status was “success”. If the video resolution was higher, for example 5K, this would fail.
Quiz time! 📚 ✏️
Troubleshooting
See the first tutorial’s troubleshooting section.
Good Job!
🤙 Congratulations 🤙
You can now send any of the other BLE commands detailed in the Open GoPro documentation in a similar manner.
To see how to parse more complicate responses, proceed to the next tutorial.